import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:my_flutter_core/config/url_config.dart';
import 'package:my_flutter_core/constants/http_method.dart';
import 'package:my_flutter_core/extensions/string_extension.dart';
import 'package:my_flutter_core/models/error_details.dart';
import 'package:my_flutter_core/models/token.dart';
import 'package:my_flutter_core/models/user.dart';
import 'package:my_flutter_core/services/app_error_report.dart';
import 'package:my_flutter_core/services/auth_service.dart';
import 'package:my_flutter_core/services/auth_state_service.dart';
import 'package:my_flutter_core/services/http_response_code_manager.dart';
import 'package:my_flutter_core/services/token_service.dart';
import 'package:my_flutter_core/utils/connectivity_utils.dart';
import 'package:my_flutter_core/utils/device_utils.dart';
import 'package:my_flutter_core/utils/system_utils.dart';
import 'package:my_flutter_core/utils/ui_interaction_utils.dart';
import 'package:path/path.dart';

typedef UploadProgressCallback = void Function(int progress);
typedef SuccessCallback = void Function(Map<String, dynamic> response);
typedef FailureCallback = void Function(Map<String, dynamic> error);

/// Http request service which interacts with backend service.
///
/// This service handles any common task. Such like token authentication, http error handling.
class HttpRequestService {
  // Defines when http request should raise timeout exception.
  // The short timeout duration works in release mode only.
  static const int _httpRequestTimeoutInSeconds = kReleaseMode ? 60 : 9999;

  static const String _httpResponseSuccessKeyName = 'success';
  static const String _httpResponsePayloadKeyName = 'payload';

  static Completer? _refreshingTokenCompleter;
  static AuthStateService authStateService = AuthStateService.instance;

  final String _hostBaseUrl = UrlConfig.apiHostBaseUrl;
  final String _httpBaseUrl = UrlConfig.apiHttpBaseUrl;

  /// Combines the provided [httpBaseUrl] and relative [url].
  String combineUrl(String httpBaseUrl, String url) {
    if (httpBaseUrl.isNotEmpty) {
      // Converts the input url to lower case just in case.
      // url = url.toLowerCase();

      // Checks if the input url already has a leading slash.
      if (!url.startsWith('/')) {
        // Appends the leading slash if needed.
        url = '/$url';
      }

      // Checks if the input url already starts with the base url.
      if (url.startsWith(httpBaseUrl)) {
        // If already contains the base url.
        return url;
      } else {
        // Appends the base url if needed.
        return httpBaseUrl + url;
      }
    } else {
      return url;
    }
  }

  /// Combines the relative [url] with default base url.
  String combineUrlWithDefault(String url) {
    return combineUrl(_httpBaseUrl, url);
  }

  /// Makes the http get request.
  ///
  /// Example:
  /// ```dart
  /// get(
  ///      '/product/list',
  ///      params: {
  ///        'pageSize': 20,
  ///        'pageIndex': 2,
  ///      },
  ///    )
  /// ```
  Future<HttpResponseResult> get(
    String url, {
    Map<String, dynamic>? params,
  }) async {
    return makeRequest(HttpMethod.get, _hostBaseUrl, _httpBaseUrl, url, params);
  }

  /// Makes the http post request.
  ///
  /// Example:
  /// post(
  ///      '/order/add',
  ///      body: {
  ///        'name': 'New Order',
  ///        'price': 100,
  ///      },
  ///    )
  Future<HttpResponseResult> post(
    String url, {
    Map<String, dynamic>? body,
  }) async {
    return makeRequest(HttpMethod.post, _hostBaseUrl, _httpBaseUrl, url, body);
  }

  /// Makes the http put request.
  ///
  /// Example:
  /// put(
  ///      '/order/save',
  ///      body: {
  ///        'name': 'New Order',
  ///        'price': 100,
  ///      },
  ///    )
  Future<HttpResponseResult> put(
    String url, {
    Map<String, dynamic>? body,
  }) async {
    return makeRequest(HttpMethod.put, _hostBaseUrl, _httpBaseUrl, url, body);
  }

  /// Makes the http put request.
  ///
  /// Example:
  /// patch(
  ///      '/order/save',
  ///      body: {
  ///        'name': 'New Order',
  ///        'price': 100,
  ///      },
  ///    )
  Future<HttpResponseResult> patch(
    String url, {
    Map<String, dynamic>? body,
  }) async {
    return makeRequest(HttpMethod.patch, _hostBaseUrl, _httpBaseUrl, url, body);
  }

  /// Makes the http put request.
  ///
  /// Example:
  /// delete(
  ///      '/order/delete',
  ///      body: {
  ///        'name': 'New Order',
  ///        'price': 100,
  ///      },
  ///    )
  Future<HttpResponseResult> delete(
    String url, {
    Map<String, dynamic>? body,
  }) async {
    return makeRequest(
      HttpMethod.delete,
      _hostBaseUrl,
      _httpBaseUrl,
      url,
      body,
    );
  }

  /// [flutter_uploader] does not work on Android 10. The behavior is the uploading action is not triggered.
  /// Uploading via base64 is alternative option to do it at this moment.
  Future<HttpResponseResult> uploadByBase64(
    String url,
    File file, {
    Map<String, dynamic>? body,
  }) async {
    if (await file.exists()) {
      final base64Content = base64Encode(file.readAsBytesSync());
      body ??= {};

      body.addAll({
        'filename': basename(file.path),
        'base64Content': base64Content,
      });

      return post(url, body: body);
    } else {
      return HttpResponseResult(false);
    }
  }

  /// It does NOT work at this moment.
  /// Working on [baseDirectory] parameter of UploadTask class.
  Future<void> upload(
    String url,
    File file, {
    Map<String, dynamic>? body,
    void Function(double)? onProgress,
    void Function()? onComplete,
    void Function()? onFailed,
  }) async {
    // Uses this get action to tigger auto refresh token logic if needed.
    await get(UrlConfig.tokenCheckUrl);

    // /data/user/0/com.opennews.openplatform.family_expense/cache/image_cropper_1689235346406.jpg
    // print(file.path);

    // Defines the multi-part upload task (subset of parameters shown).
    final task = UploadTask(
      headers: await createRequestHeader(),
      url: url,
      baseDirectory: BaseDirectory.applicationLibrary,
      directory: file.parent.path,
      filename: basename(file.path),
      fileField: 'file',
      updates: Updates.statusAndProgress,
      requiresWiFi: false,
    );

    // Start upload, and wait for result. Show progress and status changes while uploading.
    final taskStatusUpdate = await FileDownloader().upload(
      task,
      onProgress: onProgress,
    );

    if (taskStatusUpdate.status == TaskStatus.complete) {
      onComplete?.call();
    } else if (taskStatusUpdate.status == TaskStatus.failed) {
      showToast('文件 ${file.path} 上传失败');
      onFailed?.call();
    }
  }

  /// Uses background_downloader package to download file.
  Future<void> download({
    required String url,
    required BaseDirectory baseDirectory,
    required String directory,
    bool openFileOnComplete = false,
    String? filename,
    void Function(double)? onProgress,
    void Function()? onComplete,
    void Function()? onFailed,
    void Function()? onNotFound,
  }) async {
    // Requests the external storage usage permission and checks on permission request result.
    // If it has already been granted before, nothing happens.
    if (!url.startsWith('http')) {
      url = prepareUri(_hostBaseUrl, _httpBaseUrl, url)!.toString();

      // Uses this get action to tigger auto refresh token logic if needed.
      await get(UrlConfig.tokenCheckUrl);
    }

    // Defines the download task (subset of parameters shown)
    var task = DownloadTask(
      headers: await createRequestHeader(),
      url: url,
      baseDirectory: baseDirectory,
      directory: directory,
      filename: filename,
      updates: Updates.statusAndProgress,
      requiresWiFi: false,
      retries: 1,
      allowPause: true,
    );

    // Takes filename which provided by the server. It is used with server api that returns file stream instead of resource.
    if (filename.isNullOrEmpty) {
      task = await task.withSuggestedFilename(unique: true);
    }

    final fileDownloader = FileDownloader();

    // Starts download, and wait for result. Show progress and status changes while downloading.
    final taskStatusUpdate = await fileDownloader
        .configureNotification(
          running: TaskNotification('正在下载...', filename ?? ''),
          complete: TaskNotification('下载完成', filename ?? ''),
          error: TaskNotification('下载失败', filename ?? ''),
          paused: TaskNotification('下载暂停', filename ?? ''),
          progressBar: true,
          tapOpensFile: true,
        )
        .download(task, onProgress: onProgress);

    if (taskStatusUpdate.status == TaskStatus.complete) {
      showToast('下载已完成，如未出现安装界面可点击通知栏手动安装。');

      if (openFileOnComplete) {
        fileDownloader.openFile(task: taskStatusUpdate.task);
      }

      onComplete?.call();
    } else if (taskStatusUpdate.status == TaskStatus.failed) {
      showToast('文件 ${filename ?? ''} 下载失败');
      onFailed?.call();
    } else if (taskStatusUpdate.status == TaskStatus.notFound) {
      showToast('文件 ${filename ?? ''} 不存在');
      onNotFound?.call();
    }
  }

  /// Prepares the uri for http request, especially, for http get request.
  /// Because there is no way you can pass in the params to http.get method directly.
  ///
  /// The [hostBaseUrl] likes 'http://192.168.1.159:8080'.
  /// The [httpBaseUrl] likes '/we-shop-api'.
  /// The [url] likes '/product/list'.
  ///
  /// The full example code:
  /// ```dart
  ///   _prepareUri(
  ///     'http://192.168.1.159:8080',
  ///     '/we-shop-api',
  ///     '/product/list',
  ///     {
  ///       'id': 10,
  ///       'name': 'Darvis',
  ///     },
  ///   )
  /// ```
  Uri? prepareUri(
    String hostBaseUrl,
    String httpBaseUrl,
    String url, {
    Map<String, dynamic>? params = const {'': ''},
  }) {
    const httpPrefix = 'http://';
    const httpsPrefix = 'https://';

    // IMPORTANT!!!
    // We are doing the parsing because the input params type is [Map<String, dynamic>].
    // But the Uri constructor only takes [Map<String, String>].
    // If you use the mismatched type the code will fail without any exception.
    Map<String, String>? parsedParams = params?.map(
      (key, value) => MapEntry(key, value.toString()),
    );

    // The host url likes 'http://192.168.1.159:8080'.
    hostBaseUrl = hostBaseUrl.toLowerCase();

    // The [Uri] will be used for http get method to pass the params.
    Uri? uri;

    // The [Uri] constructor treats http and https differently.
    if (hostBaseUrl.startsWith(httpPrefix)) {
      uri = Uri.http(
        hostBaseUrl.replaceFirst(httpPrefix, ''),
        combineUrl(httpBaseUrl, url),
        parsedParams,
      );
    } else if (hostBaseUrl.startsWith(httpsPrefix)) {
      uri = Uri.https(
        hostBaseUrl.replaceFirst(httpsPrefix, ''),
        combineUrl(httpBaseUrl, url),
        parsedParams,
      );
    }

    return uri;
  }

  /// Creates the customized http request header.
  Map<String, String> createContentTypeRequestHeader() {
    return {HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8'};
  }

  /// Creates the customized http request header.
  Future<Map<String, String>> createAuthorizationRequestHeader() async {
    final header = <String, String>{};

    // If the access token exists then it will be attached to the http header.
    // IMPORTANT!!!
    // Even the url you are going to visit is fully opened (it means permitAll in the config),
    // if you still attach the access token the validation rule will be applied by default.
    if (await TokenService.hasAccessToken) {
      header[HttpHeaders.authorizationHeader] =
          '${await TokenService.bearer} ${await TokenService.activeAccessToken}';
    }

    return header;
  }

  /// Creates the customized http request header.
  Future<Map<String, String>> createRequestHeader() async {
    final header = await createAuthorizationRequestHeader();
    header.addAll(createContentTypeRequestHeader());

    return header;
  }

  /// Makes the http request based on [httpMethod].
  /// It will do the token check and try to refresh it if it can.
  ///
  /// DO NOT DELETE THE COMMENTS IN THE METHOD. THESE ARE FOR TESTING.
  ///
  /// Previous version uses sleep function to wait and retry when token is refreshing. This can be migrated to pure JavaScript code.
  /// Current version uses Completer class to notify when the refreshing token is done. This is similar with Angular Observable class.
  Future<HttpResponseResult> makeRequest(
    String httpMethod,
    String hostBaseUrl,
    String httpBaseUrl,
    String url,
    Map<String, dynamic>? data,
  ) async {
    Uri? uri;
    late http.Response response;

    // If device not connected to any network.
    if (await hasNetworkConnection() == false) {
      showMessage('当前无网络连接');
      return Future.value(HttpResponseResult(false));
    }

    // For debug purpose only.
    // print(
    //     'Ready to request at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}');

    try {
      final headers = await createRequestHeader();

      if (httpMethod == HttpMethod.get) {
        // Gets the [Uri] to call the [http.get] for passing the params.
        uri = prepareUri(hostBaseUrl, httpBaseUrl, url, params: data);

        // Gets the http get request response.
        response = await http
            .get(uri!, headers: headers)
            .timeout(const Duration(seconds: _httpRequestTimeoutInSeconds));
      } else {
        // Gets the [Uri] to call the http request.
        uri = prepareUri(hostBaseUrl, httpBaseUrl, url);

        Future<http.Response> Function(
          Uri url, {
          Map<String, String>? headers,
          Object? body,
          Encoding? encoding,
        })
        httpRequest = http.post;

        if (httpMethod == HttpMethod.put) {
          httpRequest = http.put;
        } else if (httpMethod == HttpMethod.patch) {
          httpRequest = http.patch;
        } else if (httpMethod == HttpMethod.delete) {
          httpRequest = http.delete;
        }

        response = await httpRequest(
          uri!,
          headers: headers,
          body: json.encode(data),
        ).timeout(const Duration(seconds: _httpRequestTimeoutInSeconds));
      }

      // For debug purpose only.
      // print(
      //     'Got response with code ${response.statusCode} at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}');

      final hasRefreshToken = await TokenService.hasRefreshToken;

      // If the [response.statusCode] is [HttpStatus.unauthorized] means current user is not logged in or the access token has expired.
      // If the refresh token exists at this moment and current refreshing token process is not running we will try to refresh the new access token.
      // We determine whether we need to refresh the token by checking the response. So we don't have to check on token info, such like expiration, expired date.
      if (response.statusCode == HttpStatus.unauthorized && hasRefreshToken) {
        // For debug purpose only.
        // print(
        //     'Checking on await TokenService.isAccessTokenExpired: ${await TokenService.isAccessTokenExpired} and '
        //     'at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}. '
        //     'Token.localDateExpired: ${(await TokenService.token).localDateExpired}');

        final isAccessTokenExpired = await TokenService.isAccessTokenExpired;

        // If the access token is expired and current refreshing token process is not running we need to refresh the token before we retry.
        // Need to check on [TokenService.isAccessTokenExpired] even we've got [HttpStatus.unauthorized] for concurrent requests.
        // In the testing we found these info:
        // - r0 ready to refreshing token at 14:55:10.817
        // - r1 ready to request at 14:55:12.192
        // - r0 finished refreshing token at 14:55:12.204
        // - r1 got response with code 401 at 14:55:12.225 (This happened after r0 refreshed the token)
        // - r1 ready to refreshing token at 14:55:12.226 (This caused duplicate refreshing token request)
        // Checking on [TokenService.isAccessTokenExpired] to avoid duplicate refreshing token request even the duplicate request does not any harm.
        if (isAccessTokenExpired && _refreshingTokenCompleter == null) {
          // For debug purpose only.
          // print(
          //     'Ready to refresh token at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}');

          // Creates the [Completer] instace for notification.
          _refreshingTokenCompleter = Completer();

          var isRefreshingTokenSuccessful = false;

          try {
            // For debug purpose only.
            // print(
            //     'Starts refreshing token at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}');

            // Tries to refresh the token.
            isRefreshingTokenSuccessful = await refreshToken();

            // Notifies all consumers who consuming completer's future.
            _refreshingTokenCompleter!.complete();

            // For debug purpose only.
            // print(
            //     'Finished refreshing token at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}. '
            //     'Token.localDateExpired: ${(await TokenService.token).localDateExpired}');
          } finally {
            _refreshingTokenCompleter = null;
          }

          // If refreshing token process is successful then we nee to retry the original request.
          // Otherwise just return the result.
          if (isRefreshingTokenSuccessful) {
            return makeRequest(httpMethod, hostBaseUrl, httpBaseUrl, url, data);
          } else {
            return checkHttpResponse(response);
          }
        } else {
          if (_refreshingTokenCompleter != null) {
            // For debug purpose only.
            // print(
            //     'Retries at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}');

            // When multi-requests on going then when first one is refreshing the token the others have to wait and retry.
            // For debug purpose only.
            // print('Waiting for completer.');

            await _refreshingTokenCompleter!.future;

            // For debug purpose only.
            // print('Completer completes.');

            return makeRequest(httpMethod, hostBaseUrl, httpBaseUrl, url, data);
          } else {
            // For debug purpose only.
            // print(
            //     'Stops retry at ${formatDate(getCurrentDateTime(), 'HH:mm:ss.SSS')}. errorDetails: ${errorDetails.message}.');

            // If error returns that means the token has been revoked from server.
            // In this case the server prevents this client to access.
            // We need to stop retry.
            return checkHttpResponse(response);
          }
        }
      } else {
        return checkHttpResponse(response);
      }
    } on TimeoutException {
      showMessage('网络连接超时，服务可能无响应或您的网络无法正常访问，请稍后重试。');
      return HttpResponseResult(false);
    } on SocketException {
      showMessage('网络连接失败，服务可能无响应或您的网络无法正常访问，请稍后重试。');
      return HttpResponseResult(false);
    } catch (e, s) {
      showMessage('服务数据处理发生未知异常，请稍后重试。');
      AppErrorReport().send(e, s);
      return HttpResponseResult(false);
    }
  }

  /// Refreshes the token if it can. It returns [true] if the refreshing is successful.
  Future<bool> refreshToken() async {
    // If the refreshing token process is successful.
    var isRefreshingTokenSuccessful = false;

    final headers = createContentTypeRequestHeader();

    // The uri for refreshing token.
    final uri = prepareUri(
      _hostBaseUrl,
      _httpBaseUrl,
      '/authentication/refresh-token',
    )!;

    // Prepares the post body.
    Map<String, dynamic> body = {
      'refreshToken': await TokenService.activeRefreshToken,
      'deviceInfo': (await getDeviceInfo())!.toJson(),
      'systemInfo': (await getSystemInfo()).toJson(),
    };

    // Makes http post to refresh the token.
    final response = await http
        .post(uri, headers: headers, body: json.encode(body))
        .timeout(const Duration(seconds: _httpRequestTimeoutInSeconds));

    // Checks if the request is successful.
    if (response.statusCode == HttpStatus.ok) {
      // Gets data from response body.
      final data = getBodyData(response.body);

      // Gets the payload from body data.
      final result = data[_httpResponsePayloadKeyName];

      // Converts the server data to user instance.
      final user = User.fromJson(result);

      // Saves the user data retrieved from server.
      authStateService.saveUserInfo(user);

      // Creates Token instance.
      final token = Token.fromServerJson(result);

      // Saves the new token.
      await TokenService.saveToken(token);

      // Checks if current user is in collection. If it is that means current users enabled quick action.
      // We just update the user and token data in the collection just in case the server refreshed the token.
      if (await authStateService.isUserInCollection(user)) {
        authStateService.saveUserToCollection(user);
        TokenService.saveTokenForUserInCollection(user);
      }

      isRefreshingTokenSuccessful = true;

      // print('Token refreshed');
    }

    // Returns the refreshing token process result.
    // If it is [true] the original request will be re-requested.
    return isRefreshingTokenSuccessful;
  }

  /// Checks on [response] to see if it is successful or not.
  ///
  /// IMPORTANT!!!
  /// If this method is not async then throwing error from here will not be caught by [runZonedGuarded] in main.dart.
  ///
  /// If it is successful then we will convert it to [Map<String, dynamic>] format.
  /// If the status code is [HttpStatus.unauthorized] we will try to refresh the access token if we can.
  Future<HttpResponseResult> checkHttpResponse(http.Response response) async {
    HttpResponseResult? httpResponseResult;

    // Checks if the response is successful or not.
    switch (response.statusCode) {
      case HttpStatus.ok:
        httpResponseResult = getResponseResult(response.body);
        break;
      case HttpStatus.noContent:
        httpResponseResult = HttpResponseResult(true);
        break;
      case HttpStatus.unauthorized:
        handleUnauthorizedError(response);
        break;
      case HttpStatus.forbidden:
        showMessage('抱歉，您被禁止该访问。');
        break;
      case HttpStatus.conflict:
      case HttpStatus.badRequest:
        handleBadRequestErrror(response);
        break;
      case HttpStatus.notFound:
        showMessage('当前服务不可访问');
        break;
      default:
        handleGeneralError(response);
        break;
    }

    // If the [httpResponseResult] means error happens.
    httpResponseResult ??= HttpResponseResult(false);

    return httpResponseResult;
  }

  /// Gets the result from the [body] and converts it to [Map<String, dynamic>] format.
  HttpResponseResult getResponseResult(String body) {
    Map<String, dynamic> res = getBodyData(body);
    final httpResponseResult = HttpResponseResult(true);
    httpResponseResult.success = res[_httpResponseSuccessKeyName];
    httpResponseResult.payload = res[_httpResponsePayloadKeyName];

    return httpResponseResult;
  }

  /// By now the spring security rest returns [HttpStatus.unauthorized] for different reasons.
  /// We need to check into the header to see what exactly happened.
  /// Handles the error of the [HttpStatus.unauthorized].
  void handleUnauthorizedError(http.Response response) {
    final errorDetails =
        getBodyErrorDetails(response) ?? getHeaderErrorDetails(response)!;

    if (errorDetails.message.isNotNullOrEmpty) {
      // Displays message to user.
      // Actually we did not display message like 'user not found' or 'password is incorrect'.
      // This is for void brute force attack. If the attacker does NOT know the login id or password is correct that would make the attach more difficult.
      showMessage(errorDetails.message!);
    } else {
      showMessage('登录已过期、没有访问权限或角色被修改，请重新登录。');
    }

    logUserOut();
  }

  void handleBadRequestErrror(http.Response response) {
    final Map<String, dynamic> bodyData = getBodyData(response.body);

    final String msg = bodyData[_httpResponsePayloadKeyName] != null
        ? bodyData[_httpResponsePayloadKeyName].toString()
        : bodyData.toString();

    showMessage(msg);
  }

  /// By now the spring security rest returns [HttpStatus.unauthorized] for different reasons.
  /// We need to check into the header to see what exactly happened.
  /// Handles the error of the [HttpStatus.unauthorized].
  void handleGeneralError(http.Response response) {
    ErrorDetails? errorDetails;

    try {
      errorDetails =
          getBodyErrorDetails(response) ?? getHeaderErrorDetails(response)!;
    } catch (_) {
      // Just make sure error will not stop processing.
    }

    var errorMessage = '服务处理发生问题';

    if (errorDetails?.message?.isNotEmpty == true) {
      errorMessage += '：${errorDetails!.message}。';
    }

    showMessage(errorMessage);
  }

  Map<String, dynamic> getBodyData(String body) {
    if (body.isNotEmpty) {
      // Decodes the response body from string to map.
      final bodyJson = json.decode(body);

      // Parses Json object to Map<String, dynamic>.
      return Map<String, dynamic>.from(bodyJson);
    }

    return {};
  }

  /// Gets the response error from body.
  ErrorDetails? getBodyErrorDetails(http.Response response) {
    final Map<String, dynamic> bodyData = getBodyData(response.body);
    ErrorDetails? errorDetails = getErrorDetails('message', bodyData);

    return errorDetails;
  }

  /// Gets response error from header.
  ErrorDetails? getHeaderErrorDetails(http.Response response) {
    return getErrorDetails('Error-Code', response.headers);
  }

  /// Gets the error message from [map] which might be the response body or header.
  /// The [key] which used to find in the map to see if it contains error code.
  ErrorDetails? getErrorDetails(String key, Map<String, dynamic> map) {
    String? errorMessage;
    String? errorCode;

    // Tries to get the error key from http header if the header key contains [key].
    // Uses orElse parameter to slient 'No Element' error if no match found.
    final errorKey = map.keys.firstWhereOrNull(
      (p) => p.toLowerCase().contains(key),
    );

    // If error code exists.
    if (errorKey.isNotNullOrEmpty) {
      // Gets the code by the key.
      errorCode = map[errorKey!];

      // Gets the message by code.
      errorMessage = HttpResponseCodeManager.getCodeMessage(errorCode!);

      return ErrorDetails(errorCode, errorMessage);
    }

    return null;
  }

  /// Shows message to user if anything goes wrong.
  void showMessage(String msg) {
    showToast(msg);
  }

  /// Logs user out.
  Future<void> logUserOut() async {
    // Gets current logged in user.
    final user = await authStateService.currentUser;

    if (user != null) {
      removeUserFromQuickLoginCollection(user);

      // Logs user out.
      AuthService().logout();
    }
  }

  /// Removes user from quick login collection. Especially this is for token issue.
  Future<void> removeUserFromQuickLoginCollection(User user) async {
    // Removes user from quick login collection.
    authStateService.removeFromUserInfoCollection(user);

    // Removes user's token from quick login collection.
    TokenService.removeTokenForUserInCollection(user);
  }
}

/// Used as the final http response result.
/// The [success] property used to indicate if the request is successful or not.
class HttpResponseResult {
  HttpResponseResult(this.success);

  bool success;
  dynamic payload;
}
